package com.atguigu.tingshu.common.login;

import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.user.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component
public class GuiguLoginAspect {
    @Autowired
    RedisTemplate redisTemplate;
    /*
        鉴权的切面 通知方法：使用环绕通知
            为什么？
                1、目标方法执行前鉴权，需要到redis中查询登录状态
                        - 可以将查询到的用户id 使用ThreadLocal存储
                2、放行后 目标方法执行时 可能要用用户信息(userId)
                        - 可以通过ThreadLocal获取用户id
                3、目标方法执行结束，为了防止内存泄露
                        - 可以将ThreadLocal中缓存的数据移除
     */
    @Around("execution(* com.atguigu.tingshu.*.api.*.*(..))" +
            " && @annotation(guiguLogin)")
    public Object around(ProceedingJoinPoint joinPoint , GuiguLogin guiguLogin) throws Throwable {//guiguLogin表示接收目标方法上的注解对象
        try {
            UserInfo userInfo = null;
            String token = null;
            //1、登录校验
            // 获取请求头中的token: RequestContextHolder会在请求进入时将ServletRequestAttributes对象设置到ThreadLocal中
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            token = request.getHeader("token");
            //1、如果目标方法需要登录： 先获取token，获取失败抛出异常 否则接收token
            if(guiguLogin.required()){//需要鉴权
                if(StringUtils.isEmpty(token)){//token不存在 直接抛出异常 让前端重新登录
                    throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
                }

            }
            //2、token不为空：根据token获取redis中缓存的登录状态
            //            UserInfo
            if(!StringUtils.isEmpty(token)){
                userInfo = (UserInfo) redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX + token);
            }
            //3、当目标方法用户需要登录 并且 用户信息为空 抛出异常
            //判断登录状态是否有效
            if(null == userInfo && guiguLogin.required()){ //登录状态失效  直接抛出异常 让前端重新登录
                throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
            }
            //4、如果获取到用户信息 缓存用户信息
            if(null != userInfo){
                AuthContextHolder.setUserId(userInfo.getId());
                AuthContextHolder.setUsername(userInfo.getNickname());
            }
            //5、执行目标业务方法
            Object result = joinPoint.proceed();
            //7、返回执行结果
            return result;
        } finally {
            //6、删除缓存
            AuthContextHolder.removeUserId();
            AuthContextHolder.removeUsername();
        }
    }

}
